package jupiter.channel;

import jupiter.filters.FilterException;
import jupiter.message.Message;
import jupiter.util.*;
import jupiter.servlets.MessageServlet;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import jupiter.util.MemCache;
import org.json.*;
import org.mortbay.util.ajax.Continuation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.UUID;
/**
 * Basically adds and removes itself from DomainChannel
 * @author Justin
 *
 */
public class Channel implements ChannelInterface{
	
	//private ArrayList<Address> blocks = new ArrayList<Address>();
	//private ArrayList<Address> only= new ArrayList<Address>();
	//private ArrayList<Address> subscriptionAddresses= new ArrayList<Address>();  // a list of channels you listen to right away.  Is this even needed?  probably not
	protected Address address;
	protected String secret;
	protected JSONObject data;
	private ArrayList<ChannelInterface> fromChannels = new  ArrayList<ChannelInterface>(); //channels I get messages from
	private ArrayList<ChannelInterface> toChannels= new  ArrayList<ChannelInterface>(); //channels I send messages to
	private boolean keepList  = false;
	private boolean isOnDomainChannel = false;
	private boolean admin = false;
	static Logger logger = LoggerFactory.getLogger(Channel.class);
	
	long startedAt;
	//private String name;
	/**
	 * Creates a channel in the db from a message like:
	 * 
	 * {
	 *	  type: "channel"
	 *	  action: "create"
	 *	  from: "jmvc.com@jabbify.com"
	 *	  to: "justin#jmvc.com@jabbify.com"
	 *	  data : {
	 *	    subscriptions: ['#jmvc.com@jabbify.com'] //which channels I subscribe to
	 *	  }
	 * }
	 * @param m
	 */
	
	public Channel( Message m)  throws FilterException  {
		//address = m.to();
		JSONObject json = m.json;

		try{
			data = json.getJSONObject("data");
			address = new Address(data.getString("address"));
			//json.put("to", address.toString());
			
			if(!data.has("secret"))
				data.put("secret", UUID.randomUUID().toString());
			secret = data.getString("secret");
			
		}catch(JSONException e){
			new FilterException("{error: 'no address'}");
		}
		//need to validate making this channel is ok
		
		prepare();
		try{
			(new DBConnection() {
				public Object run() throws SQLException{
					prepareStatement("INSERT INTO channels "+
							"(address, secret, data, created_at, updated_at, domain_id, name, user_id) "+
							"VALUE(?, ?, ?, NOW(),NOW(),? ,?, ?);");
					pstmt.setString(1, address.toString());
					pstmt.setString(2, secret);
					pstmt.setString(3, toJSON(true).toString());
					pstmt.setString(4, null);
					pstmt.setString(5, null);
					pstmt.setString(6, null);
					pstmt.executeUpdate();
					return true;
				}
			}).exec(null);
		}catch(SQLException sqle){
			throw new FilterException("{warning: 'channel or name already exists'}");
		}
	}
	
	/**
	 * Conforms the data
	 * @throws FilterException
	 */
	void prepare() throws FilterException {
		try{
			if( data.has("keepList") ){
				keepList = data.getBoolean("keepList");
			}
			if(! data.has("subscriptions") ) data.put("subscriptions", new JSONArray());
			if(! data.has("publicInfo") ) data.put("publicInfo", new JSONObject());
			//if(data.has("name"))
			//	name = data.getString("name");
		}catch(JSONException e){
			System.out.println(e);
			throw new FilterException("{error: 'no data in message'}");
		}
		
		//loadIntoArray(data,"blocks", blocks);
		//loadIntoArray(data,"only", only);
		//loadIntoArray(data,"subscriptions", subscriptionAddresses);
	}
	/**
	 * Load from db, or from MemCache, or if create by listen, anyone
	 * @param a
	 */
	public Channel( Address a) throws FilterException {
		address = a;
		//first check domain if we can createByListen
		String jsonstr = null;
		// if its a domain
		String channelData = null;
		channelData = (String) MemCache.mcc.get(address.toString());
		// check the memcache
		// if found, skip
		// else, do the lookup and save (if its a domain)
		if(channelData == null) {
			try{
				jsonstr = (String) (new DBConnection() {
					public Object run() throws SQLException{
						prepareStatement("Select data from channels WHERE address = ?;");
						pstmt.setString(1, address.toString());
						executeQuery();
						boolean has = rs.next();
						if(!has) return null;
						return  rs.getString("data");
						
					}
				}).exec(null);
			}catch(SQLException sqle){
				throw new FilterException("{error: 'something is wrong, contact Jabbify'}");
			}
			MemCache.mcc.set(address.toString(), jsonstr);
		} else {
			jsonstr = channelData;
		}
		Domain dn = Global.getDomain(address);
		
		try{
			if(jsonstr == null){
				if(!dn.ruleCreateByListen()) throw new FilterException("{error: 'cant find addressable channel'}");
				
				data = new JSONObject();
				data.put("address", address.toString());
				data.put("secret", UUID.randomUUID().toString());
				if(a.channel().compareTo("/") == 0){ //default to keep list for simple client
					data.put("keepList", true);
				}
			}else{
				data = new JSONObject(jsonstr);
			}

			
			secret = data.getString("secret") ;
			startedAt = new java.util.Date().getTime();
			prepare(); //makes sure we have public info
			
			//data.getJSONObject("publicInfo").put("startedAt", );
			//listen to everyone in subscriptions
			//System.out.println("  SUBSCRIBING TO "+data.getJSONArray("subscriptions").toString());
			//Sync.s(this,"new");
			//synchronized (this) {
				//Sync.r(this,"new");
				for(Address channelAddress : toArrayList(data, "subscriptions") ){
					Channel parent = dn.getOrCreateChannel( channelAddress.hashlessAddress() );
					parent.addListener(this, true);
				};
				//Sync.u(this,"new");
			//}
			
			
			
		}catch(JSONException e){
			throw new FilterException("{error: 'cant create channel "+address()+"'}");
		}
	

		
	}

	private ArrayList<Address> toArrayList(JSONObject d, String type) throws FilterException{
		ArrayList<Address> list = new ArrayList<Address>();
		try{
			if(d.has(type)){
				JSONArray arr = d.getJSONArray(type);
				int l = arr.length();
				for(int i = 0; i < l; i++){
					list.add( new Address( arr.getString(i)  ));
				}
			}
		}catch(JSONException e){	
			Channel.logger.error("JSON Error", e);
		}
		return list;
	}
	private HashMap<String, Address> toHashMap(JSONObject d, String type) throws FilterException{
		HashMap<String, Address> list = new HashMap<String, Address>();
		try{
			if(d.has(type)){
				JSONArray arr = d.getJSONArray(type);
				int l = arr.length();
				for(int i = 0; i < l; i++){
					Address a = new Address( arr.getString(i)  );
					list.put(a.toString(), a);
				}
			}
		}catch(JSONException e){
			Channel.logger.error("JSON Error", e);
		}
		return list;
	}
	
	public void invalidateSessions(){
		Session s;
		for(ChannelInterface n: toChannels){
			s = (Session) n;
			System.out.println("REMOVESESS: "+s.getClass().getName());
			if(s.getClass().getName() == "jupiter.channel.Session")
				s.invalidate();
		}
	}
	
	public boolean isBlocked(Message m){
		return false;
	}
	public JSONObject toJSON(boolean secret){
		try{
			JSONObject d = new JSONObject(data.toString());
			if( !secret)
				d.remove("secret");
			return d;
		}catch(JSONException e){
			return data;
		}
		
	}
	public JSONObject sendMessage(Message m){
		if(isBlocked(m)) return null;
		//Sync.s(this,"sendMessage");
		synchronized (toChannels) {
			//Sync.r(this,"sendMessage");
			for(ChannelInterface n: toChannels){
				n.sendMessage(m);
			}
			//Sync.u(this,"sendMessage");
		}
		
		
		return null;
	}
	/**
	 * Adds a channel listening to this one.  You need at least one listener to be added to the
	 * Domain channel. (this is not true for sessions)
	 */
	public void addListener(ChannelInterface n, boolean callback) throws FilterException{
		//add to tochannels, add yourself to n's fromchannels, 
		boolean add_to_domain = false;

		synchronized (toChannels) {

			add_to_domain = toChannels.size() == 0;
			if(add_to_domain) {
				Global.getDomain(address).addChannel(this);
				isOnDomainChannel = true;
			}
			
			if(n.canSendMessagesTo( this))
				throw new FilterException("{error: 'cycle produced!'}");

			if(toChannels.contains(n)) 
				throw new FilterException("{warning: 'already added channel'}");

			toChannels.add(n);
			

		}
		send("addListener", n);
		if(callback)
			n.addedAsListener(this);
		// need to check if we keep a channel list, if so broadcast to user list
	}
	public void send(String action, ChannelInterface n){
		//System.out.println("SENDING -"+ (keepList ? "yes" : "no")+"- "+address() );
		if(keepList){
			JSONObject jo = new JSONObject();
			try{
				jo.put("type", "channel");
				jo.put("action", action);
				jo.put("to", address().toString());
				jo.put("data", n.publicInfo());
			}catch(JSONException e){
				Channel.logger.error("JSON Error trying to send", e);
			}
			MessageServlet.subscriber.sendText(jo.toString());
		}
	}
	
	
	public void removeListener(ChannelInterface channel, boolean callback) throws FilterException{
		synchronized (toChannels) {
			toChannels.remove(channel);
			if(toChannels.size() == 0){
				Global.getDomain(address).removeChannel( this);
				isOnDomainChannel = false;
				//destroy();
			}
			
			//get a list of who we are listening to, 
			//on each one of those nodes, 
		}
		if(isOnDomainChannel == false){
			synchronized(fromChannels){
				for(ChannelInterface f : fromChannels){
					f.removeListener(this, false);  // this will remove the channels
				}
				fromChannels.clear();
			}
		}
		send("removeListener", channel);
		if(callback) channel.removedAsListener(this);

	}
	public boolean canSendMessagesTo(ChannelInterface channel){
		boolean retVal = false;
		synchronized (toChannels) {
			//Sync.r(this,"sendsMessagesTo");
			
			for(ChannelInterface fn : toChannels){ //can only be addressables
				if(fn == channel || fn.canSendMessagesTo(  channel) ) {
					retVal = true;
					break;
				}
			}
			//Sync.u(this,"sendsMessagesTo");
		}
		return retVal;
	}
	public void addedAsListener(ChannelInterface channel){
		synchronized (fromChannels) { fromChannels.add(channel); }
	}
	public void removedAsListener(ChannelInterface channel) throws FilterException {
		synchronized (fromChannels) { fromChannels.remove(channel); }
	}
	public void destroy() throws FilterException {
		synchronized(toChannels){
			for(ChannelInterface t : toChannels){
				this.removeListener(t, false);
			}
			toChannels.clear();
			Global.getDomain(address).removeChannel( this);
			isOnDomainChannel = false;
		}
		synchronized(fromChannels){
			for(ChannelInterface f : fromChannels){
				f.removeListener(this, false);  // this will remove the channels
			}
			fromChannels.clear();
		}
		
		//when destroy is called, you should never have anything listening 
		/*synchronized(toChannels){
			for(Channel t : toChannels){
				this.removeListener(t, false);
			}
			toChannels.clear();

		}*/
	}
	
	
	

	public Address address(){
		return address;
	}
	
	static public JSONObject remove(Message m) throws FilterException{
		//make sure we have the secret
		try{
			 m.data.getString("secret");
		}catch(JSONException e){
			throw new FilterException("{error: 'no secret'}");
		}
		try{
			String jsonstr = (String) (new DBConnection() {
				public Object run(Object m) throws SQLException{
					Message message = (Message) m;
					prepareStatement("DELETE FROM channels WHERE "+
							" address = ? and secret = ?;");
					pstmt.setString(1, message.to().toString() );
					try{
						pstmt.setString(2,  message.data.getString("secret") );
					}catch(JSONException e){
						Channel.logger.error("JSON Error", e);
					}
					
					pstmt.executeUpdate();
					return null;
				}
			}).exec(m);
		}catch(SQLException sqle){
			throw new FilterException("{error: 'unable to remove channel'}");
		}
		//get and remove channel
		ChannelInterface n = Global.getDomain(m.to()).getChannel(m.to());
		if(n!= null)
			n.destroy();
		return m.json;
	}
	public JSONObject map() throws JSONException {
		JSONObject json = new JSONObject();
		synchronized(toChannels){
			for(ChannelInterface t : toChannels){
				json.put(t.address().toString(), t.map() );
			}
		}
		return json;
	}
	public void resume(){
		System.out.println("YOU SHOULD NOT BE RESUMING An ADDRESSABLE "+address());
	}
	public JSONObject publicInfo(){

		try{
			JSONObject info =  new JSONObject(data.getJSONObject("publicInfo").toString());
			info.put("address", address().toString());
			info.put("admin", admin);
			return info; //have to have data for address
		}catch(JSONException e){
			return new JSONObject();
		}
		
	}

	/**
	 * Returns the 'new' data for this object if needed
	 * null if nothing
	 */
	public JSONObject subscribeAndUpdate(Address a) throws FilterException {

		boolean alreadyAdded = false;
		JSONObject copy = toJSON(true);
		try{
			String address = a.toString();
			synchronized (fromChannels) { //you are only going to chage this
				
				JSONArray  subs = copy.getJSONArray("subscriptions");
				
				for(int i = 0 ; i < subs.length(); i++){
					String val = subs.getString(i);
					if(val.compareTo(address) == 0){
						alreadyAdded = true;
						break;
					}
				}
				if(!alreadyAdded)
					subs.put(address);
			}
		}catch(JSONException e){
			Channel.logger.error("JSON Error", e);
		}
		if(!alreadyAdded){
			update(copy);
			return copy;
		}

		return null;
	}
	
	public JSONObject unsubscribeAndUpdate(Address a) throws FilterException {

		boolean isSubscribed = false;
		JSONObject copy = toJSON(true);
		try{
			String address = a.toString();
			JSONArray newSubs = new JSONArray();
			synchronized (fromChannels) { //you are only going to change this
				JSONArray  subs = copy.getJSONArray("subscriptions");
				//check that address isn't in subscriptions
				for(int i = 0 ; i < subs.length(); i++){
					String val = subs.getString(i);
					if(val.compareTo(address) == 0){
						isSubscribed = true;
					}else
						newSubs.put(val);
				}
			}
			copy.put("subscriptions",newSubs);
		}catch(JSONException e){
			
		}
		
		if(isSubscribed){
			update(copy);
			return copy;
		}

		return null;
	}
	public JSONObject updatePublicInfo(JSONObject d) throws FilterException {
		//setup new data
		try{
			JSONObject newData = new JSONObject(data.toString());
			//need to copy startedAt
			newData.put("publicInfo", d.getJSONObject("publicInfo"));
			newData.put("secret", d.getString("secret"));
			
			return update(newData);
		}catch(JSONException e){
			throw new FilterException("{error: 'unable to update public info'");
		}
		
	}

	public String name(){
		if(data.has("publicInfo")){
			try {
				JSONObject pi = (JSONObject) data.get("publicInfo");
				String name = pi.getString("name");
				return name;
			} catch (JSONException e) {
				Channel.logger.error("JSON Error", e);
			}
		}
		return null;
	}	
	/**
	 * Update simply changes the DB, it does not modify the in memory representation of the AN
	 * that happens later in ChannelFilter:updateInbox
	 * this should check that the user has the 'rights' to update, then perform the update
	 */
	public JSONObject update(JSONObject d) throws FilterException {
		//make sure secret is right

		//make sure subscriptions aren't duplicated
		try{
			//check secret
			if( secret.compareTo( d.getString("secret")) != 0 )
				throw new FilterException("{error: 'incorrect secret'}");
			
			JSONArray  subs = d.getJSONArray("subscriptions");
			HashMap<String, String> hasher = new HashMap<String, String>();
			JSONArray  oks = new JSONArray();
			for(int i = 0 ; i < subs.length(); i++){
				String val = subs.getString(i);
				if(!hasher.containsKey(val))
						oks.put(val);
				hasher.put(val, val);
			}
			d.put("subscriptions", oks);
			if(!d.has("publicInfo")) d.put("publicInfo", new JSONObject());  //we should have prepare work on anything
			JSONObject pi = d.getJSONObject("publicInfo");
			pi.put("startedAt", startedAt  );

		}catch(JSONException e){
			Channel.logger.error("JSON Error", e);
		}
		Integer affected;
		try{
			affected = (Integer) (new DBConnection() {
				public Object run(Object dO) throws SQLException{
					JSONObject d = (JSONObject) dO;
					
					prepareStatement("UPDATE channels "+
							"SET updated_at = NOW(), data = ? "+
							"WHERE address = ?;");
					pstmt.setString(1, d.toString());
					pstmt.setString(2, address.toString());
					return new Integer(pstmt.executeUpdate());
				}
			}).exec(d);
		}catch(SQLException sqle){
			throw new FilterException("{warning: 'something went wrong with update'}");
		}
		if(affected != 1)
			throw new FilterException("{error: 'something went wrong with update "+affected+"'}");
		//its possible that other JettyNodes want this too, they will have to listen
		
		MemCache.mcc.set(address.toString(), d.toString());
		
		return d;
	}
	/**
	 * Updates channel with message
	 */
	public JSONObject modify(JSONObject d) throws FilterException {
		//you can assume that the message is ok and saved to the db
		
		//we should go through and make sure the new rules are being followed.  Update is how you would
		//add someone to be blocked
		//we need to tell that there is new data
		
		//check if new public info is not the same as old
		boolean publicInfoChanged = false;
		try{
			publicInfoChanged = data.getJSONObject("publicInfo").toString().compareTo(d.getJSONObject("publicInfo").toString() ) != 0;
		}catch(JSONException e){
			Channel.logger.error("JSON Error", e);
		}
		data = d;
		
		//add / remove fromChannels that aren't there
		
	
		HashMap<String, Address> subscriptionHash = null; //first make subscriptions a hash for faster matching
		

		subscriptionHash = toHashMap(data,"subscriptions");

		Domain dn = Global.getDomain(address);
		ArrayList<ChannelInterface> channelsToBeRemoved = new ArrayList<ChannelInterface>();
		synchronized (fromChannels) {
			//go through from channels
			for(ChannelInterface n : fromChannels){
				if( subscriptionHash.containsKey(n.address().toString()) ){ 
					subscriptionHash.remove(n.address().toString());		//still has subscription
				}else{
					//not in it anymore, remove
					n.removeListener(this, false); //doesn't like being modified while being iterated
					channelsToBeRemoved.add(n);
				}
				
			}
			for(ChannelInterface rn : channelsToBeRemoved){
				fromChannels.remove(rn);
			}

			//go through what's left and add listeners
			for(Iterator<Map.Entry <String, Address>> iter = subscriptionHash.entrySet().iterator(); iter.hasNext();){
				Map.Entry<String, Address> entry = iter.next();
				Address adding = entry.getValue();
				dn.getOrCreateChannel(adding).addListener((ChannelInterface)this, true);
			}
			
			//would need to tell each of these
			if(publicInfoChanged){
				try{
					JSONObject jo = new JSONObject();
					jo.put("type", "channel");
					jo.put("action", "updatePublicInfo");
					jo.put("from", address().toString());
					jo.put("data", publicInfo());
					for(ChannelInterface n : fromChannels){
						jo.put("to", n.toString());
						MessageServlet.subscriber.sendText(jo.toString());
					}
				}catch(JSONException e){
					Channel.logger.error("JSON Error", e);
				}
			}
			
		}
		
		return null;
	}
	public String toString(){
		return address().toString();
	}

}
